/*
 *  Copyright 2007 Blandware (http://www.blandware.com)
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */
package com.blandware.atleap.webapp.util.core;

import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.context.ApplicationContext;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.commons.validator.GenericValidator;

import javax.servlet.ServletContext;
import java.util.*;

import com.blandware.atleap.webapp.struts.HeritableComponentDefinition;
import com.blandware.atleap.webapp.struts.PossibleContainer;
import com.blandware.atleap.webapp.struts.ContainerList;
import com.blandware.atleap.webapp.struts.HeritableDefinitionsFactory;
import com.blandware.atleap.webapp.exception.InconsistentTilesConfigException;
import com.blandware.atleap.common.Constants;
import com.blandware.atleap.common.util.QueryInfo;
import com.blandware.atleap.service.core.LayoutManager;
import com.blandware.atleap.service.exception.BeanNotFoundException;
import com.blandware.atleap.service.exception.BeanAlreadyExistsException;
import com.blandware.atleap.model.core.Layout;
import com.blandware.atleap.model.core.TileContainer;
import com.blandware.atleap.model.core.TileListComponent;

/**
 * <p>Works with tile containers.</p>
 * <p><a href="TileContainerUtil.java.html"><i>View source</i></a></p>
 *
 * @author Roman Puchkovskiy <a href="mailto:roman.puchkovskiy@blandware.com">
 *         &lt;roman.puchkovskiy@blandware.com&gt;</a>
 * @version $Revision: 1.15 $ $Date: 2007/05/27 13:48:18 $
 */
public class TileContainerUtil {
    /**
     * Commons logging instance
     */
    protected transient final Log log = LogFactory.getLog(TileContainerUtil.class);

    /**
	 * Servlet context key under which instance of this class is saved
	 */
	private static final String INSTANCE_KEY = "com.blandware.atleap.webapp.util.TileContainerUtil.INSTANCE";

    /**
     * Servlet context
     */
    protected ServletContext servletContext;

    /**
     * Application context
     */
    protected ApplicationContext ctx = null;

    /**
     * Layout manager
     */
    protected LayoutManager layoutManager = null;

    /**
     * Factory
     */
    protected HeritableDefinitionsFactory factory = null;

    /**
     * Contains current definitions map
     */
    protected Map definitions = new HashMap();

    /**
     * Contains initial definitions map
     */
    protected Map initialDefinitions = new HashMap();

    /**
     * Set of containers. Each element is a full list identifier.
     */
    protected Set containers = new HashSet();

    /**
     * Definitions which are components
     */
    protected Map components = new HashMap();

    /**
     * Set of root definitions for layouts
     */
    protected Set rootDefinitions = new HashSet();

    /**
     * Default constructor.
     */
    private TileContainerUtil() {
    }

    /**
     * Constructor.
     *
     * @param servletContext current servlet context
     */
    private TileContainerUtil(ServletContext servletContext) {
        this();
        this.servletContext = servletContext;
        ctx = WebApplicationContextUtils.getRequiredWebApplicationContext(servletContext);
        layoutManager = obtainLayoutManager();
    }

    /**
	 * Returns instance of TileContainerUtil corresponding to a given servlet
     * context.
	 *
     * @param servletContext Servlet context
	 * @return Instance of TileContainerUtil
	 */
	public static TileContainerUtil getInstance(ServletContext servletContext) {
		TileContainerUtil tileContainerUtil
                = (TileContainerUtil) servletContext.getAttribute(INSTANCE_KEY);
		if ( tileContainerUtil == null ) {
			tileContainerUtil = new TileContainerUtil(servletContext);
			servletContext.setAttribute(INSTANCE_KEY, tileContainerUtil);
		}
		return tileContainerUtil;
	}

    /**
     * Initializes object using definitions map.
     *
     * @param definitions map of definitions
     * @param factory Factory - for feedback
     */
    public void initialize(Map definitions,
                           HeritableDefinitionsFactory factory) {
        this.factory = factory;

        // Save definitions
        this.definitions = definitions;

        // Make definitions copy
        initialDefinitions = copyDefinitions(definitions);

        // Get layouts
        Collection layouts = obtainLayouts();

        // Create virtual definitions by layouts
        createVirtualDefinitions(layouts);

        // Now process what we got
        processDefinitions();

        // Check that containers config is consistent
        checkContainersConsistency(layouts);

        // Syncronizing DB state
        synchronizePersistedDefinitions(false);

        // Correcting factory data
        factory.setDefinitions(this.definitions);

        if (log.isInfoEnabled()) {
            log.info("Tiles config initialized");
        }
    }

    /**
     * Creates virtual definitions - those, that are not in initial config.
     * @param layouts collection of layouts for which to create virtual
     * definitions
     */
    protected void createVirtualDefinitions(Collection layouts) {
        Iterator layoutsIterator = layouts.iterator();

        Map definitionsToLayouts = new HashMap();
        while (layoutsIterator.hasNext()) {
            Layout layout = (Layout) layoutsIterator.next();
            definitionsToLayouts.put(layout.getDefinition(), layout);
        }

        layoutsIterator = layouts.iterator();
        while (layoutsIterator.hasNext()) {
            Layout layout = (Layout) layoutsIterator.next();
            createVirtualDefinitionsForLayout(layout, definitionsToLayouts,
                                              new HashSet());
        }
    }

    /**
     * Creates virtual definitions for layout and its ancestors (for ancestors
     * first).
     *
     * @param layout layout to process
     * @param definitionsToLayouts mapping of definitions to layouts
     * @param processedLayouts layouts that are already processed
     */
    protected void createVirtualDefinitionsForLayout(Layout layout,
                                                     Map definitionsToLayouts,
                                                     Set processedLayouts) {
        if (!processedLayouts.contains(layout)
                && layout.getVirtual().booleanValue()) {
            boolean unknownDefinitionEncountered = false;
            // process parent layout first, if exists (ancestors are processed
            // recursively too)
            String parentDefinition = layout.getDefinitionParent();
            Layout parentLayout = null;
            if (definitionsToLayouts.get(parentDefinition) != null) {
                // there's a layout with parentDefinition definition
                parentLayout
                        = (Layout) definitionsToLayouts.get(parentDefinition);
            } else {
                // maybe there's a layout higher
                // there are no virtual definitions, so we can just climb up
                while (definitionsToLayouts.get(parentDefinition) == null) {
                    HeritableComponentDefinition def = (HeritableComponentDefinition) definitions.get(parentDefinition);
                    if (def != null) {
                        if (def.getExtends() == null) {
                            // reached root: no parent layout
                            break;
                        } else {
                            parentDefinition = def.getExtends();
                        }
                    } else {
                        // no such definition, this is bad
                        if (log.isErrorEnabled()) {
                            log.error("Virtual layout with ID=" + layout.getId()
                                    + " references definition named '"
                                    + parentDefinition
                                    + "' which does not exist");
                        }
                        unknownDefinitionEncountered = true;
                        break;
                    }
                }
                if (!unknownDefinitionEncountered) {
                    parentLayout = (Layout) definitionsToLayouts.get(parentDefinition);
                }
            }
            if (parentLayout != null) {
                createVirtualDefinitionsForLayout(parentLayout,
                        definitionsToLayouts, processedLayouts);
            }

            // processed ancestors, process layout itself
            createDefinition(layout.getDefinition(),
                    layout.getDefinitionParent());
            if (!GenericValidator.isBlankOrNull(layout.getCpDefinition())
                    && !layout.getDefinition().equals(layout.getCpDefinition())) {
                createDefinition(layout.getCpDefinition(),
                        layout.getCpDefinitionParent());
            }

            // processed
            processedLayouts.add(layout);
        }
    }

    /**
     * Processes definitions: figures out, which are containers, which are
     * puttable.
     */
    protected void processDefinitions() {
        containers = new HashSet();
        components = new HashMap();

        Iterator i = definitions.values().iterator();
        while (i.hasNext()) {
            HeritableComponentDefinition definition
                    = (HeritableComponentDefinition) i.next();
            if (log.isDebugEnabled()) {
                log.debug("Processing definition named '"
                        + definition.getName() + "'");
            }

            processDefinition(definition);
        }
    }

    /**
     * Processes definition: determines whether this is component, remembers
     * its containers.
     *
     * @param definition definition to process
     */
    protected void processDefinition(HeritableComponentDefinition definition) {
        processDefinitionAsComponent(definition);
        processDefinitionContainers(definition);
    }

    /**
     * Checks if definition is puttable and if so, puts it into
     * <b>components</b> map.
     *
     * @param definition definition to check
     */
    protected void processDefinitionAsComponent(
            HeritableComponentDefinition definition) {
        List containersList = definition.getContainersList();
        if (!containersList.isEmpty()) {
            // it's declared to be a component
            if (log.isDebugEnabled()) {
                log.debug("Found definition which is declared to be a component, it's name is '"
                        + definition.getName()
                        + "', allowed containers are:"
                        + containersList);
            }
            Iterator containersIterator = containersList.iterator();
            while (containersIterator.hasNext()) {
                String container = (String) containersIterator.next();
                boolean ok = true;
                int index = container.indexOf(Constants.TILE_CONTAINER_IDENTIFIER_SEPARATOR);
                if (index == -1) {
                    // no delimiter
                    if (log.isErrorEnabled()) {
                        log.error("Full container identifier '"
                                + container
                                + "' should contain "
                                + Constants.TILE_CONTAINER_IDENTIFIER_SEPARATOR
                                + " delimiter, skipping");
                    }
                    ok = false;
                } else {
                    String defName = container.substring(0, index);
                    String listName = container.substring(index + 1);
                    HeritableComponentDefinition def
                            = (HeritableComponentDefinition) definitions.get(defName);
                    if (def == null) {
                        // no such definition
                        if (log.isErrorEnabled()) {
                            log.error("Full container identifier '"
                                    + container
                                    + "' references unknown definition: '"
                                    + defName + "', skipping");
                        }
                        ok = false;
                    } else {
                        Object list = def.getAttribute(listName);
                        if (list == null) {
                            // no such attribute
                            if (log.isErrorEnabled()) {
                                log.error("Full container identifier '"
                                        + container
                                        + "' references unknown attribute: '"
                                        + listName + "', skipping");
                            }
                            ok = false;
                        } else {
                            if (!(list instanceof PossibleContainer)) {
                                // not a possible container
                                if (log.isErrorEnabled()) {
                                    log.error("Full container identifier '"
                                            + container
                                            + "' references attribute: '"
                                            + listName
                                            + "', which cannot be a container, skipping");
                                }
                                ok = false;
                            }
                        }
                    }
                }
                if (!ok) {
                    // something is wrong with this container, remove it
                    // from list
                    containersIterator.remove();
                }
            }
            if (!containersList.isEmpty()) {
                if (log.isDebugEnabled()) {
                    log.debug("Definition is really a component, it's name is '"
                            + definition.getName()
                            + "', allowed containers are:"
                            + containersList);
                }
                components.put(definition.getName(), definition);
            } else {
                if (log.isDebugEnabled()) {
                    log.debug("Definition is NOT a component: no containers left in list. Name is '"
                            + definition.getName() + "'");
                }
            }
        }
    }

    /**
     * Checks if definition has containers and if so, puts their names into
     * <b>containers</b> map; such lists are made empty.
     *
     * @param definition definition to process
     */
    protected void processDefinitionContainers(
            HeritableComponentDefinition definition) {
        Map attributes = definition.getAttributes();
        Iterator attributeIterator = attributes.keySet().iterator();
        while (attributeIterator.hasNext()) {
            String name = (String) attributeIterator.next();
            Object attribute = attributes.get(name);
            if (attribute instanceof ContainerList) {
                ContainerList containerList = (ContainerList) attribute;
                if (containerList.isContainer()) {
                    String identifier = definition.getName()
                            + Constants.TILE_CONTAINER_IDENTIFIER_SEPARATOR
                            + name;
                    // it's container
                    if (log.isDebugEnabled()) {
                        log.debug("Found list which is container, it's identifier is '"
                                + identifier + "'");
                    }
                    containers.add(identifier);
                }
            }
        }
    }

    /**
     * Checks containers consistency. If there are two layouts which have access
     * to some definition (i.e. this definition is reachable for both layouts)
     * and this definition has containers in it and these layouts are not
     * ancestor and descendant, containers are inconsistent.
     * If so, exception is thrown.
     *
     * @param layouts collection of layouts for which to do check
     * @throws InconsistentTilesConfigException thrown if containers are
     * inconsistent
     */
    protected void checkContainersConsistency(Collection layouts)
            throws InconsistentTilesConfigException {
        DefinitionUtil definitionUtil = DefinitionUtil.getInstance(servletContext);

        // this will contain a mapping from layout to its reachable definitions
        // for all checked layouts (those that passed check successfully)
        Map layoutsReachableDefinitions = new HashMap();

        // this will contain names of definitions that have containers
        Set definitionsWithContainers = new HashSet();
        Iterator containersIterator = containers.iterator();
        while (containersIterator.hasNext()) {
            String identifier = (String) containersIterator.next();
            int index = identifier.indexOf(Constants.TILE_CONTAINER_IDENTIFIER_SEPARATOR);
            String defName = identifier.substring(0, index);
            definitionsWithContainers.add(defName);
        }

        Iterator layoutsIterator = layouts.iterator();
        while (layoutsIterator.hasNext()) {
            Layout layout = (Layout) layoutsIterator.next();
            String thisLayoutDefinitionName = layout.getDefinition();
            Set reachableDefinitions = getReachableDefinitions(thisLayoutDefinitionName);
            Iterator entryIterator = layoutsReachableDefinitions.entrySet().iterator();
            while (entryIterator.hasNext()) {
                Map.Entry e = (Map.Entry) entryIterator.next();
                Layout l = (Layout) e.getKey();
                Set set = (Set) e.getValue();
                Set intersection = new HashSet(set);
                intersection.retainAll(reachableDefinitions);
                List intersectionNames = definitionUtil.getDefinitionsNames(intersection);
                Set intersectionWithContainers = new HashSet(intersectionNames);
                intersectionWithContainers.retainAll(definitionsWithContainers);
                Iterator intersectionIterator = intersectionWithContainers.iterator();
                while (intersectionIterator.hasNext()) {
                    String suspicious = (String) intersectionIterator.next();
                    // Definition with this name is reachable from both layouts
                    // and has attributes which are containers. If these two
                    // layouts are not ancestor and descendant (or wice versa),
                    // config is inconsistent
                    String thatLayoutDefinitionName = l.getDefinition();
                    if (!definitionUtil.isAncestor(thisLayoutDefinitionName, thatLayoutDefinitionName, definitions)
                            && !definitionUtil.isAncestor(thatLayoutDefinitionName, thisLayoutDefinitionName, definitions)) {
                        throw new InconsistentTilesConfigException("Layouts with definitions '"
                                + thisLayoutDefinitionName + "' and '"
                                + thatLayoutDefinitionName
                                + "' have access to definition named '"
                                + suspicious + "' which has containers");
                    }
                }
            }

            // inconsistences between this layout and already processed ones
            // are not found, so add it to mapping
            layoutsReachableDefinitions.put(layout, reachableDefinitions);
        }
    }

    /**
     * Makes a deep copy of definitions map.
     *
     * @param orig original map
     * @return new map, which is copy of original
     */
    protected Map copyDefinitions(Map orig) {
        Map map = new HashMap();
        Iterator i = orig.entrySet().iterator();
        while (i.hasNext()) {
            Map.Entry entry = (Map.Entry) i.next();
            Object key = entry.getKey();
            HeritableComponentDefinition value
                    = (HeritableComponentDefinition) entry.getValue();
            map.put(key, new HeritableComponentDefinition(value));
        }
        return map;
    }

    /**
     * Obtains layout manager.
     *
     * @return layout manager
     */
    protected LayoutManager obtainLayoutManager() {
        LayoutManager layoutManager
                = (LayoutManager) ctx.getBean(Constants.LAYOUT_MANAGER_BEAN);
        return layoutManager;
    }

    /**
     * Returns collection of all layouts.
     *
     * @return collection of all layouts
     */
    protected Collection obtainLayouts() {
        QueryInfo queryInfo = new QueryInfo();
        return layoutManager.listLayouts(queryInfo);
    }

    /**
     * Synchronizes containers redifinitions in DB with real definitions from
     * external source (for instance, from XML file). Synchronization is made
     * for all layouts found in DB.
     *
     * @param flushing if <tt>true</tt>, initial config will be used as primary:
     * resulting DB state will reflect initial config. If <tt>false</tt>, DB
     * state will prevail: for instance, initial values of containers will be
     * same as in DB.
     */
    public void synchronizePersistedDefinitions(boolean flushing) {
        synchronizePersistedDefinitions(obtainLayouts(), flushing);
    }

    /**
     * Synchronizes containers redifinitions in DB with real definitions from
     * external source (for instance, from XML file). Synchronization is made
     * for a given collection of layouts. 
     *
     * @param layouts collection of layouts for which to do synchronization 
     * @param flushing if <tt>true</tt>, initial config will be used as primary:
     * resulting DB state will reflect initial config. If <tt>false</tt>, DB
     * state will prevail: for instance, initial values of containers will be
     */
    protected void synchronizePersistedDefinitions(Collection layouts,
                                                   boolean flushing) {
        rootDefinitions = new HashSet();
        Iterator layoutsIterator = layouts.iterator();
        // loop through layouts
        while (layoutsIterator.hasNext()) {
            Layout layout = (Layout) layoutsIterator.next();
            synchronizePersistedDefinitionsForLayout(layout, flushing);
        }
    }

    /**
     * Synchronizes containers redifinitions for given layout in DB with real
     * definitions from external source (for instance, from XMl file).
     *
     * @param layout layout
     * @param flushing if true, initial config will be used as primary:
     * resulting DB state will reflect initial config. If false, DB state
     * will prevail: for instance, initial values of containers will be same as
     * in DB.
     */
    protected void synchronizePersistedDefinitionsForLayout(Layout layout,
                                                            boolean flushing) {
        String defName = layout.getDefinition();
        rootDefinitions.add(defName);
        Set reachableDefinitions = getReachableDefinitions(defName);

        List containers
                = layoutManager.findTileContainersByLayoutId(layout.getId());
        Iterator containersIterator = containers.iterator();
        Set persistedContainers = new HashSet();
        Map persistedMap = new HashMap();
        // loop through containers in DB for this layout
        while (containersIterator.hasNext()) {
            TileContainer container
                    = (TileContainer) containersIterator.next();
            String name = container.getDefinitionName();
            String identifier = container.getFullIdentifier();
            if (!this.containers.contains(identifier)
                    || !isDefinitionInSet(name, reachableDefinitions)) {
                // remove not-more-valid container
                if (log.isDebugEnabled()) {
                    log.debug("Deleting container with identifier '"
                            + identifier
                            + "' because it is not present in config for '"
                            + layout.getName() + "' layout");
                }
                try {
                    layoutManager.deleteTileContainer(container.getId());
                } catch (BeanNotFoundException e) {
                    // swallow, because we can be here only if no such
                    // container exists more, so our job is already done
                }
            } else {
                // this container is in DB and still valid
                // just put it into map: we'll process it later
                persistedContainers.add(identifier);
                persistedMap.put(identifier, container);
            }
        }

        // now create those containers that are not in DB
        containersIterator = this.containers.iterator();
        while (containersIterator.hasNext()) {
            String identifier = (String) containersIterator.next();
            int index = identifier.indexOf(Constants.TILE_CONTAINER_IDENTIFIER_SEPARATOR);
            String name = identifier.substring(0, index);
            String listName = identifier.substring(index + 1);
            HeritableComponentDefinition definition
                    = (HeritableComponentDefinition) definitions.get(name);
            ContainerList containerList
                    = (ContainerList) definition.getAttribute(listName);

            boolean isReachable = isDefinitionInSet(name,
                                                    reachableDefinitions);
            boolean isInDB = persistedContainers.contains(identifier);
            if (isReachable && !isInDB) {
                // reachable, but not in DB: create
                if (log.isDebugEnabled()) {
                    log.debug("Creating a container with identifier '"
                            + identifier
                            + "' because it is reachable for layout '"
                            + layout.getName() + "' but not in DB");
                }
                containerList.clear();
                TileContainer container = new TileContainer(name, listName);
                // Fill initial list state
                setContainerInitialState(container);
                // Propagate to descendants
                propagateContainerToDescendants(container, containerList);
                try {
                    layoutManager.createTileContainer(container,
                                                  layout.getId());
                } catch (BeanAlreadyExistsException e) {
                    // swallow: someone has already created this
                } catch (BeanNotFoundException e) {
                    if (log.isErrorEnabled()) {
                        log.error("Cannot create a container: no layout;"
                                + e);
                    }
                }
            } else if (isReachable && isInDB) {
                containerList.clear();
                TileContainer container
                        = (TileContainer) persistedMap.get(identifier);
                boolean changed = false;
                if (flushing) {
                    // flushing: remove everything, then fill with initial
                    container.getTileListComponents().clear();
                    setContainerInitialState(container);
                    changed = true;
                } else {
                    // check components: some of them may become invalid
                    // for valid components, modify list
                    Iterator i = container.getTileListComponents().iterator();
                    while (i.hasNext()) {
                        TileListComponent component
                                = (TileListComponent) i.next();
                        String componentName = component.getName();
                        if (components.get(componentName) == null) {
                            // component is out of date, remove
                            if (log.isDebugEnabled()) {
                                log.debug("From tile container '"
                                        + identifier
                                        + "' removing out-of-date component: '"
                                        + componentName + "'");
                            }
                            i.remove();
                            changed = true;
                        } else {
                            // component is valid
                            containerList.add(componentName);
                        }
                    }
                }
                propagateContainerToDescendants(container,
                                                containerList);
                if (changed) {
                    try {
                        layoutManager.updateTileContainer(container);
                    } catch (Exception e) {
                        if (log.isErrorEnabled()) {
                            log.error("Cannot update tile container, ID='"
                                    + identifier + "': " + e);
                        }
                    }
                }
            }
        }
    }

    /**
     * Fills container with components according to initial config. Definition
     * list for container is also updated.
     *
     * @param container tile container to work with
     */
    protected void setContainerInitialState(TileContainer container) {
        String defName = container.getDefinitionName();
        String listName = container.getListName();
        HeritableComponentDefinition initialDefinition
                = (HeritableComponentDefinition) initialDefinitions.get(defName);
        HeritableComponentDefinition definition
                = (HeritableComponentDefinition) definitions.get(defName);        
        List initialList = (List) initialDefinition.getAttribute(listName);
        List list = (List) definition.getAttribute(listName);
        list.clear();
        Iterator i = initialList.iterator();
        while (i.hasNext()) {
            Object elem = i.next();
            if (elem instanceof String) {
                String s = (String) elem;
                HeritableComponentDefinition compDef = (HeritableComponentDefinition) initialDefinitions.get(s);
                if (containerMatchesComponent(container, compDef)) {
                    container.addListComponent(new TileListComponent(s));
                    list.add(s);
                }
            }
        }
    }

    /**
     * Updates config which corresponds to a given container.
     *
     * @param container tile container
     */
    public void updateContainerConfig(TileContainer container) {
        String defName = container.getDefinitionName();
        String listName = container.getListName();
        HeritableComponentDefinition definition
                = (HeritableComponentDefinition) definitions.get(defName);
        List list = (List) definition.getAttribute(listName);
        list.clear();
        Iterator i = container.getTileListComponents().iterator();
        while (i.hasNext()) {
            TileListComponent component = (TileListComponent) i.next();
            String componentName = component.getName();
            if (containerMatchesComponent(container, (HeritableComponentDefinition) initialDefinitions.get(componentName))) {
                // valid, put into list
                list.add(componentName);
            }
        }
        // propagate this container to descendants
        propagateContainerToDescendants(container, list);

        // pass to factory
        factory.setDefinitions(definitions);
    }

    /**
     * Reloads tiles config: discards all changes made through admin console.
     * Implementation note: actually, files are not parsed again, but ALMOST
     * initial version of config is used (which was obtained from config files).
     * ALMOST means that restored config may slightly differ from initial:
     * virtual definitions are preserved.
     */
    public void reloadTilesConfig() {
        synchronizePersistedDefinitions(true);
    }

    /**
     * Returns definitions which are nested inside definition with a given
     * name. This set contains this definition too.
     *
     * @param name definition name
     * @return set of definitions
     */
    protected Set getReachableDefinitions(String name) {
        return getReachableDefinitions(name, new HashSet());
    }

    /**
     * Adds to set definitions which are nested inside definition with a given
     * name. The set contains this definition too.
     *
     * @param name definition name
     * @param set already built part of set
     * @return set of definitions
     */
    protected Set getReachableDefinitions(String name, Set set) {
        HeritableComponentDefinition definition
                = (HeritableComponentDefinition) initialDefinitions.get(name);
        if (definition != null) {
            set.add(definition);
            Iterator i = definition.getAttributes().values().iterator();
            while (i.hasNext()) {
                Object attrValue = i.next();
                if (attrValue instanceof List) {
                    List list = (List) attrValue;
                    Iterator j = list.iterator();
                    while (j.hasNext()) {
                        String value = (String) j.next();
                        set = getReachableDefinitionsIfNotInSet(value, set);
                    }
                } else if (attrValue instanceof String) {
                    String value = (String) attrValue;
                    set = getReachableDefinitionsIfNotInSet(value, set);
                }
            }
        }
        return set;
    }

    /**
     * Adds reachable definitions for definition with a given name to set, if
     * such definition exists and is not in set yet. 
     *
     * @param name name of definition
     * @param set set of already found reachable definitions
     * @return set of reachable definitions
     */
    protected Set getReachableDefinitionsIfNotInSet(String name, Set set) {
        Object child = initialDefinitions.get(name);
        if (child != null) {
            // Found nested definition
            HeritableComponentDefinition chDef
                    = (HeritableComponentDefinition) child;
            if (!set.contains(chDef)) {
                // If not yet processed, process
                set = getReachableDefinitions(chDef.getName(), set);
            }
        }
        return set;
    }

    /**
     * Returns whether definition with given name is contained in a set of
     * definitions.
     *
     * @param name name of definition
     * @param definitions definitions set
     * @return true if definition is contained
     */
    protected boolean isDefinitionInSet(String name, Set definitions) {
        return definitions.contains(initialDefinitions.get(name));
    }

    /**
     * Returns a list of names of definitions that may be put into a given
     * container.
     *
     * @param container container to check
     * @return list of puttable definitions names
     */
    public List getPossibleContainerComponentsNames(TileContainer container) {
        List result = new ArrayList();
        Iterator i = components.values().iterator();
        while (i.hasNext()) {
            HeritableComponentDefinition definition
                    = (HeritableComponentDefinition) i.next();
            if (containerMatchesComponent(container, definition)) {
                result.add(definition.getName());
            }
        }
        return result;
    }

    /**
     * Returns a list of component names. This list is built based on given
     * list of components.
     *
     * @param components list of components
     * @return list of names
     */
    public List getComponentNames(List components) {
        List result = new ArrayList();
        Iterator i = components.iterator();
        while (i.hasNext()) {
            TileListComponent component = (TileListComponent) i.next();
            result.add(component.getName());
        }
        return result;
    }

    /**
     * Returns a list which contains TileListComponentInfo instances. This list
     * is built based on given list of component names.
     *
     * @param names list of component names
     * @return list of TileListComponentInfo instances
     */
    public List wrapComponentNames(List names) {
        List result = new ArrayList();
        Iterator i = names.iterator();
        while (i.hasNext()) {
            String name = (String) i.next();
            HeritableComponentDefinition component
                    = (HeritableComponentDefinition) components.get(name);
            String description = component.getDescription();
            if (GenericValidator.isBlankOrNull(description)) {
                description = name;
            }
            boolean isCopyable = component.isCopyable();
            result.add(new TileListComponentInfo(name, description,
                                                 isCopyable));
        }
        return result;
    }

    /**
     * Returns a description of a tile container.
     *
     * @param container tile container which description to obtain
     * @return tile container description
     */
    public String getContainerDescription(TileContainer container) {
        String name = container.getDefinitionName();
        String listName = container.getListName();
        HeritableComponentDefinition definition
                = (HeritableComponentDefinition) initialDefinitions.get(name);
        PossibleContainer list
                = (PossibleContainer) definition.getAttribute(listName);
        return list.getDescription();
    }

    /**
     * Returns true if container or any of its ancestors can contain a given
     * component.
     *
     * @param container container from which to begin search
     * @param component component to check
     * @return true if container or its ancestors can contain this component
     */
    protected boolean containerMatchesComponent(TileContainer container,
            HeritableComponentDefinition component) {
        boolean result = false;
        String name = container.getDefinitionName();
        String listName = container.getListName();
        HeritableComponentDefinition definition
                = (HeritableComponentDefinition) initialDefinitions.get(name);
        do {
            String identifier = name
                    + Constants.TILE_CONTAINER_IDENTIFIER_SEPARATOR + listName;
            if (containerStrictlyMatchesComponent(identifier, component)) {
                result = true;
                break;
            }
            name = definition.getExtends();
            definition = (HeritableComponentDefinition) initialDefinitions.get(name);
        } while (name != null);
        return result;
    }

    /**
     * Returns true if container identifier by full identifier can contain
     * a given component.
     *
     * @param containerIdentifier identifier of container
     * @param component component to check
     * @return true if container can contain this component
     */
    protected boolean containerStrictlyMatchesComponent(String containerIdentifier,
            HeritableComponentDefinition component) {
        return component.getContainersList().contains(containerIdentifier);
    }

    /**
     * For each descendant of this container definition (which is not root
     * definition of another layout or its descendant), assign same list
     * value.
     *
     * @param container container from which to propagate list
     * @param list list to set
     */
    protected void propagateContainerToDescendants(TileContainer container,
                                                   List list) {
        if (log.isDebugEnabled()) {
            log.debug("Propagating container with identifier '"
                    + container.getFullIdentifier() + "', list is " + list);
        }
        String name = container.getDefinitionName();
        String listName = container.getListName();
        DefinitionUtil definitionUtil
                = DefinitionUtil.getInstance(servletContext);
        List descNamesList
                = definitionUtil.getDescendantDefinitionsNames(name,
                        initialDefinitions);
        Set descNames = new HashSet(descNamesList);
        descNames.remove(name);

        // remove definition names that are descendants but belong to
        // another layout
        Iterator i = rootDefinitions.iterator();
        while (i.hasNext()) {
            String rootDefName = (String) i.next();
            if (!rootDefName.equals(name) && descNames.contains(rootDefName)) {
                List rootDescNamesList = definitionUtil.getDescendantDefinitionsNames(rootDefName, initialDefinitions);
                descNames.removeAll(rootDescNamesList);
                descNames.remove(rootDefName);
            }
        }

        i = descNames.iterator();
        while (i.hasNext()) {
            String descName = (String) i.next();
            HeritableComponentDefinition definition
                    = (HeritableComponentDefinition) definitions.get(descName);
            List oldList = (List) definition.getAttribute(listName);
            oldList.clear();
            oldList.addAll(list);
        }
    }

    /**
     * Returns a sublist of given list of names. Returned names are names of
     * components which are not copyable.
     *
     * @param componentNames list of component names
     * @return list of non-copyable component names
     */
    public List getNonCopyableComponents(List componentNames) {
        List result = new ArrayList();
        Iterator i = componentNames.iterator();
        while (i.hasNext()) {
            String name = (String) i.next();
            HeritableComponentDefinition component
                    = (HeritableComponentDefinition) components.get(name);
            if (!component.isCopyable()) {
                result.add(name);
            }
        }
        return result;
    }

    /**
     * Creates a new definition with given parent.<br/>
     * Only changes in-memory config, does not affect containers state in DB.
     *
     * @param name definition name
     * @param parentName name of parent definition; if empty or no such
     * definition, no parent is assigned
     * @return created definition
     */
    public HeritableComponentDefinition createDefinition(String name,
                                                         String parentName) {
        if (log.isDebugEnabled()) {
            log.debug("Creating virtual definition, name is '" + name
                    + "', which extends '" + parentName + "'");
        }
        HeritableComponentDefinition definition = null;
        if (!GenericValidator.isBlankOrNull(parentName)) {
            HeritableComponentDefinition parent
                    = (HeritableComponentDefinition) initialDefinitions.get(parentName);
            if (parent != null) {
                definition = new HeritableComponentDefinition(parent);
                definition.setExtends(parentName);
            }
        }
        if (definition == null) {
            definition = new HeritableComponentDefinition();
        }
        definition.setName(name);
        initialDefinitions.put(name, definition);
        definitions.put(name, new HeritableComponentDefinition(definition));
        processDefinition(definition);
        return definition;
    }

    /**
     * Deletes definition by its name.<br/>
     * Only changes in-memory config, does not affect containers state in DB.
     *
     * @param name definition name
     */
    public void deleteDefinition(String name) {
        if (log.isDebugEnabled()) {
            log.debug("Deleting virtual definition, name is '" + name + "'");
        }
        initialDefinitions.remove(name);
        definitions.remove(name);

        Iterator i = containers.iterator();
        while (i.hasNext()) {
            String identifier = (String) i.next();
            int index = identifier.indexOf(Constants.TILE_CONTAINER_IDENTIFIER_SEPARATOR);
            String defName = identifier.substring(0, index);
            if (defName.equals(name)) {
                i.remove();
            }
        }

        components.remove(name);
        rootDefinitions.remove(name);
    }

    /**
     * Changes parent of definition with given name. Actually, old definition
     * is removed and new is created with given parent.<br/>
     * Only changes in-memory config, does not affect containers state in DB.
     *
     * @param name definition name
     * @param parentName name of parent definition
     * @return definition
     */
    public HeritableComponentDefinition changeDefinitionParent(String name,
            String parentName) {
        if (log.isDebugEnabled()) {
            log.debug("Changing virtual definition parent, name is '" + name
                    + "', new parent is '" + parentName + "'");
        }
        deleteDefinition(name);
        return createDefinition(name, parentName);
    }
}
